using System;
using System.Collections;
using System.Diagnostics;
using System.Linq;
using System.Runtime.InteropServices;
using System.Windows.Forms;
using Framework.Interop;

namespace Framework
{
    ///<summary>
    ///</summary>
    [Guid("7fc15bfb-b668-47e2-807a-2c3e565e2f15")]
    [CLSCompliant(false)]
    public class NamespaceExtension : IPersistFolder2, IShellFolder2, IShellFolderViewCB, IRemoteComputer
    {
        #region IIDs

        protected static readonly Guid IID_IDataObject = new Guid("0000010e-0000-0000-C000-000000000046");
        protected static readonly Guid IID_IDropSource = new Guid("00000121-0000-0000-C000-000000000046");
        protected static readonly Guid IID_IExtractIcon = new Guid("000214FA-0000-0000-C000-000000000046");
        protected static readonly Guid IID_IContextMenu = new Guid("000214E4-0000-0000-C000-000000000046");
        protected static readonly Guid IID_IRemoteComputer = new Guid("000214FE-0000-0000-C000-000000000046");
        protected static readonly Guid IID_IShellFolder = new Guid("000214E6-0000-0000-C000-000000000046");
        protected static readonly Guid IID_IShellFolder2 = new Guid("93F2F68C-1D1B-11d3-A30E-00C04F79ABD1");
        protected static readonly Guid IID_IShellView = new Guid("000214E3-0000-0000-C000-000000000046");

        #endregion

        #region COM register/unregister functions

        // ReSharper disable UnusedMember.Local
        [ComRegisterFunction]
        private static void Register(Type type)

        {
            new NamespaceExtensionRegistrar(type).Register();
        }

        [ComUnregisterFunction]
        private static void Unregister(Type type)
        {
            new NamespaceExtensionRegistrar(type).Unregister();
        }

        // ReSharper restore UnusedMember.Local

        #endregion

        #region Fields

        protected readonly IFolderObject FolderObj;
        protected readonly NamespaceExtension Parent;

        #endregion

        #region Constructors

        protected NamespaceExtension(IFolderObject folderObj)
        {
            if (folderObj == null)
            {
                throw new ArgumentNullException("folderObj");
            }

            this.FolderObj = folderObj;
        }

        protected NamespaceExtension(IFolderObject folderObj, NamespaceExtension parent)
        {
            if (parent == null)
            {
                throw new ArgumentNullException("parent");
            }
            if (folderObj == null)
            {
                throw new ArgumentNullException("folderObj");
            }

            this.Parent = parent;
            this.FolderObj = folderObj;
        }

        #endregion

        #region IPersist members

        ///<summary>
        ///</summary>
        ///<param name="pClassID"></param>
        public void GetClassID(out Guid pClassID)
        {
            var g = (GuidAttribute)Attribute.GetCustomAttribute(GetType(), typeof (GuidAttribute));
            pClassID = new Guid(g.Value);
        }

        #endregion

        #region IPersistFolder members

        public void Initialize(ItemIDList pidl)
        {
            this.FolderObj.SetFullPath(pidl.GetItemData());
        }
        
        #endregion

        #region IPersistFolder2 members

        void IPersistFolder2.GetCurFolder(out ItemIDList ppidl)
        {
            using (Malloc m = Shell32.GetMalloc())
            {
                ppidl = ItemIDList.Create(m, this.FolderObj.PathData);
            }
        }

        #endregion

        #region IShellFolder members

        ///<summary cref="IShellFolder.ParseDisplayName">
        ///</summary>
        public void ParseDisplayName(IWin32Window hwnd, IntPtr pbc, string pszwDisplayName, IntPtr pchEaten,
                                     out ItemIDList ppidl, IntPtr attrs)
        {
            // TODO
            throw new NotImplementedException();
        }

        ///<summary cref="IShellFolder.EnumObjects">
        ///</summary>
        public void EnumObjects(IWin32Window hwnd, EnumOptions grfFlags, out IEnumIDList ppenumIDList)
        {
            ppenumIDList = new EnumIDListImpl(grfFlags, this.FolderObj.GetItems(hwnd));
        }

        ///<summary cref="IShellFolder.BindToObject">
        ///</summary>
        ///<exception cref="COMException"></exception>
        public void BindToObject(ItemIDList pidl, IntPtr pbc, ref Guid riid, out IntPtr ppv)
        {
            if (riid.Equals(IID_IShellFolder) ||
                riid.Equals(IID_IShellFolder2))
            {
                IFolderObject obj = this.FolderObj;

                obj = pidl.GetItemData().Aggregate(obj, (current, data) => current.Restore(data));

                Type interfaceType = riid.Equals(IID_IShellFolder) ? typeof (IShellFolder) : typeof (IShellFolder2);

                ppv = Marshal.GetComInterfaceForObject(new NamespaceExtension(obj, this), interfaceType);
            }
            else if (riid.Equals(IID_IRemoteComputer))
            {
                ppv = IntPtr.Zero;
                Debug.Write(pidl);
            }
            else
            {
                throw new InvalidCastException();
            }
        }

        ///<summary cref="IShellFolder.BindToStorage">
        ///</summary>
        public void BindToStorage(ItemIDList pidl, IntPtr pbc, ref Guid riid, out IntPtr ppv)
        {
            throw new NotImplementedException();
        }

        ///<summary cref="IShellFolder.CompareIDs">
        ///</summary>
        public int CompareIDs(int lParam, ItemIDList pidl1, ItemIDList pidl2)
        {
            IEnumerator enum1 = pidl1.GetItemData().GetEnumerator();
            IFolderObject item1 = this.FolderObj;

            IEnumerator enum2 = pidl2.GetItemData().GetEnumerator();
            IFolderObject item2 = this.FolderObj;

            // this assumes enum1 and enum2 contain one item each, *or*
            // that each item in the path's CompareTo method can take
            // a Column from *this* folder object; not necessarily their
            // parent's folder object

            ColumnCollection columns = this.FolderObj.Columns;

            Column column = lParam >= 0 && lParam < columns.Count
                                ? columns[lParam]
                                : null;

            while (enum1.MoveNext())
            {
                if (enum2.MoveNext())
                {
                    item1 = item1.Restore((byte[])enum1.Current);
                    item2 = item2.Restore((byte[])enum2.Current);

                    int result = item1.CompareTo(column, item2);

                    if (result != 0)
                    {
                        return result;
                    }
                }
                else
                {
                    return -1;
                }
            }

            return 0;
        }

        ///<summary cref="IShellFolder.CreateViewObject">
        ///</summary>
        public void CreateViewObject(IWin32Window hwndOwner, ref Guid riid, out IntPtr ppv)
        {
            if (IID_IShellView.Equals(riid))
            {
                var sfvc = new ShellFolderViewCreate(this, this);
                Marshal.ThrowExceptionForHR(Shell32.SHCreateShellFolderView(ref sfvc, out ppv));

                if (ppv == IntPtr.Zero)
                {
                    throw new COMException();
                }
            }
            else if (IID_IContextMenu.Equals(riid))
            {
                ppv = Marshal.GetComInterfaceForObject(new ContextMenuImpl(null, this.FolderObj), typeof (IContextMenu));
            }
            else
            {
                throw new InvalidCastException();
            }
        }

        ///<summary cref="IShellFolder.GetAttributesOf">
        ///</summary>
        public void GetAttributesOf(uint cidl, IntPtr[] apidl, ref FolderAttributes rgfInOut)
        {
            for (int i = 0; i < cidl && rgfInOut != 0; i++)
            {
                ItemID item = ItemID.Create(apidl[i]);
                rgfInOut &= this.FolderObj.Restore(item.GetData()).Attributes;
            }
        }

        ///<summary cref="IShellFolder.GetUIObjectOf">
        ///</summary>
        public void GetUIObjectOf(IWin32Window hwndOwner, uint cidl, IntPtr[] apidl, ref Guid riid, IntPtr rgfReserved,
                                  out IntPtr ppv)
        {
            if (riid.Equals(IID_IExtractIcon) && cidl == 1)
            {
                if (cidl != 1)
                {
                    throw new ArgumentOutOfRangeException("cidl", cidl,
                                                          "Expected exactly one PIDL to retrieve IExtractIcon");
                }

                ppv =
                    Marshal.GetComInterfaceForObject(
                        new ExtractIconImpl(this.FolderObj.Restore(ItemIDList.Create(apidl[0]).GetItems()[0].GetData())),
                        typeof (IExtractIcon));
            }
            else if (riid.Equals(IID_IDataObject))
            {
                if (cidl != 1)
                {
                    throw new ArgumentOutOfRangeException("cidl", cidl,
                                                          "Expected exactly one PIDL to retrieve IDataObject");
                }

                IFolderObject obj = this.FolderObj.Restore(ItemIDList.Create(apidl[0]).GetItemData()[0]);
                DataObject dataObject = obj.DataObject;
                IntPtr pUnk = Marshal.GetIUnknownForObject(dataObject);
                Marshal.ThrowExceptionForHR(Marshal.QueryInterface(pUnk, ref riid, out ppv));
            }
            else if (riid.Equals(IID_IContextMenu))
            {
                var fos = new ArrayList();

                for (uint i = 0; i < cidl; i++)
                {
                    fos.Add(this.FolderObj.Restore(ItemID.Create(apidl[i]).GetData()));
                }

                ppv =
                    Marshal.GetComInterfaceForObject(
                        new ContextMenuImpl(this.FolderObj, (IFolderObject[])fos.ToArray(typeof (IFolderObject))),
                        typeof (IContextMenu));
            }
            else
            {
                throw new InvalidCastException();
            }
        }

        ///<summary cref="IShellFolder.GetDisplayNameOf">
        ///</summary>
        public void GetDisplayNameOf(ItemIDList pidl, NameOptions uFlags, out StrRet pName)
        {
            byte[] buf = pidl.GetItems()[0].GetData();
            IFolderObject obj = this.FolderObj.Restore(buf);
            string name = obj.GetDisplayName(uFlags);
            pName = StrRet.CreateDefault(name);
        }

        ///<summary cref="IShellFolder.SetNameOf">
        ///</summary>
        public void SetNameOf(IWin32Window hwnd, ItemIDList pidl, string pszName, NameOptions uFlags,
                              ref IntPtr ppidlOut)
        {
            try
            {
                var relative = new ArrayList();
                IFolderObject fo = this.FolderObj;

                foreach (byte[] data in pidl.GetItemData())
                {
                    relative.Add(data);
                    fo = fo.Restore(data);
                }

                var oldId = (byte[])relative[relative.Count - 1];
                relative.RemoveAt(relative.Count - 1);

                fo.SetName(hwnd, pszName, uFlags);

                byte[] newId = fo.Persist();

                if (ppidlOut != IntPtr.Zero)
                {
                    using (Malloc m = Shell32.GetMalloc())
                    {
                        var newRelativePidl = new ArrayList(relative) {newId};
                        ppidlOut = ItemIDList.Create(m, (byte[][])newRelativePidl.ToArray(typeof (byte[]))).Ptr;
                    }
                }

                byte[][] here = this.FolderObj.PathData;

                var oldAbs = new ArrayList(here);
                oldAbs.AddRange(relative);
                oldAbs.Add(oldId);

                var newAbs = new ArrayList(here);
                newAbs.AddRange(relative);
                newAbs.Add(newId);

                using (Malloc m = Shell32.GetMalloc())
                {
                    ItemIDList oldPidl = ItemIDList.Create(m, (byte[][])oldAbs.ToArray(typeof (byte[])));
                    ItemIDList newPidl = ItemIDList.Create(m, (byte[][])newAbs.ToArray(typeof (byte[])));

                    Shell32.SHChangeNotify(
                        ((fo.Attributes & FolderAttributes.Folder) == FolderAttributes.Folder)
                            ? SHChangeNotifyEvents.RenameFolder
                            : SHChangeNotifyEvents.RenameItem,
                        SHChangeNotifyFlags.IdList,
                        oldPidl.Ptr,
                        newPidl.Ptr);

                    m.Free(oldPidl.Ptr);
                    m.Free(newPidl.Ptr);
                }
            }
            catch
            {
                ppidlOut = IntPtr.Zero;
            }
        }

        #endregion

        #region IShellFolder2 members

        ///<summary cref="IShellFolder2.GetDefaultSearchGUID">
        ///</summary>
        public void GetDefaultSearchGUID(out Guid pqguid)
        {
            throw new NotImplementedException();
        }

        ///<summary cref="IShellFolder2.EnumSearches">
        ///</summary>
        public void EnumSearches(out IEnumExtraSearch ppenum)
        {
            throw new NotImplementedException();
        }

        ///<summary cref="IShellFolder2.GetDefaultColumn">
        ///</summary>
        public void GetDefaultColumn(uint dwRes, out ulong pSort, out ulong pDisplay)
        {
            ColumnCollection columns = this.FolderObj.Columns;
            pSort = 0;
            pDisplay = 0;

            for (int i = 0; i < columns.Count; i++)
            {
                if (columns[i] == columns.DefaultDisplayColumn)
                {
                    pDisplay = (ulong)i;
                }

                if (columns[i] == columns.DefaultSortColumn)
                {
                    pSort = (ulong)i;
                }
            }
        }

        ///<summary cref="IShellFolder2.GetDefaultColumnState">
        ///</summary>
        public void GetDefaultColumnState(uint iColumn, out ColumnStates pcsFlags)
        {
            Column column = this.FolderObj.Columns[(int)iColumn];
            pcsFlags = ColumnStates.None;

            if (column.Slow)
            {
                pcsFlags |= ColumnStates.Slow;
            }

            if (column.DefaultVisible)
            {
                pcsFlags |= ColumnStates.OnByDefault;
            }

            IList numericTypes = new ArrayList(new[]
                                                   {
                                                       typeof (sbyte), typeof (byte),
                                                       typeof (short), typeof (ushort),
                                                       typeof (int), typeof (uint),
                                                       typeof (long), typeof (ulong),
                                                       typeof (decimal)
                                                   });

            if (numericTypes.Contains(column.Type))
            {
                pcsFlags |= ColumnStates.TypeInt;
            }
            else if (column.Type == typeof (DateTime))
            {
                pcsFlags |= ColumnStates.TypeDate;
            }
            else
            {
                pcsFlags |= ColumnStates.TypeStr;
            }
        }

        ///<summary cref="IShellFolder2.GetDetailsEx">
        ///</summary>
        public void GetDetailsEx(ItemIDList pidl, ref SHColumnID pscid, out object pv)
        {
            throw new NotImplementedException();
        }

        ///<summary cref="IShellFolder2.GetDetailsOf">
        ///</summary>
        public void GetDetailsOf(ItemIDList pidl, uint iColumn, out ShellDetails psd)
        {
            Column column = this.FolderObj.Columns[(int)iColumn];
            psd = new ShellDetails {cxChar = column.Width, fmt = column.Format};

            if (pidl == null)
            {
                psd.str = StrRet.CreateDefault(column.Name);
            }
            else
            {
                IFolderObject obj = this.FolderObj.Restore(pidl.GetItemData()[0]);
                object @value = obj.GetColumnValue(column);
                psd.str = StrRet.CreateDefault(@value == null ? string.Empty : @value.ToString());
            }
        }

        ///<summary cref="IShellFolder2.MapColumnToSCID">
        ///</summary>
        public void MapColumnToSCID(uint iColumn, ref SHColumnID pscid)
        {
            Column column = this.FolderObj.Columns[(int)iColumn];

            if (column.PropertyIdentifier < 0)
            {
                throw new ArgumentOutOfRangeException("iColumn", iColumn, "Column does not implement a property set");
            }

            pscid.fmtid = column.FormatIdentifier;
            pscid.pid = (uint)column.PropertyIdentifier;
        }

        #endregion

        #region IShellFolderViewCB members

        ///<summary cref="IShellFolderViewCB.MessageSFVCB">
        ///</summary>
        void IShellFolderViewCB.MessageSFVCB(ShellFolderViewMessage uMsg, ref IntPtr wParam, ref IntPtr lParam)
        {
            switch (uMsg)
            {
                case ShellFolderViewMessage.GetNotify:
                    using (Malloc m = Shell32.GetMalloc())
                    {
                        wParam = ItemIDList.Create(m, this.FolderObj.PathData).Ptr;
                        lParam = new IntPtr((int)(SHChangeNotifyEvents.RenameFolder | SHChangeNotifyEvents.RenameItem));
                    }
                    break;

                default:
                    throw new NotImplementedException();
            }
        }

        #endregion

        #region IRemoteComputer Members

        ///<summary cref="IRemoteComputer.Initialize">
        ///</summary>
        void IRemoteComputer.Initialize(string pszMachine, bool bEnumerating)
        {
            this.FolderObj.RemoteComputer = pszMachine;
        }

        #endregion
    } ;
}
